import datetime
import json
import re

from threading import Thread
from pymongo import MongoClient, UpdateOne
from collections import OrderedDict

from utility.log import logger
from utility.sendcloud import SendCloud
from utility.yunpian import YunPian
from utility.monitor import Monitor
from utility.utils import get_flat_value
from conf.settings import CLOUDMONITOR_MONGODB, DATE_FMT


DB_INFO = {
    'host': CLOUDMONITOR_MONGODB
}

DETAIL_URL = 'https://console.scanv.com'


ENABLE_MODULES = OrderedDict({
    # NOTE: It will be done in order.
    'domain': {
        'has_enable': True,  # if it has an xxxEnable in balances.
        'update_dict': {'enableMonitor': False},  # mongo update sql
        'conditions': [('enableMonitor', True)],  # check key if is enabled.
        'stop_params': {'jobId': '_id', 'taskId': '*'},  # stopping task required params.
    },
    'httpMaxTask': {
        'has_enable': True,
        'update_dict': {'httpSettings.{index}.enable': False},
        'conditions': [('enableMonitor', True), ('httpSettings.{index}.enable', True)],
        'stop_params': {'jobId': '_id', 'taskId': 'httpSettings.{index}.taskId'},
    },
    'sitePortrait': {
        'has_enable': False,
        'update_dict': {'vulSettings.enableSitePortraitTriggerMonitor': False},
        'conditions': [('enableMonitor', True), ('vulSettings.enableSitePortraitTriggerMonitor', True)],
        'stop_params': {'jobId': '', 'taskId': ''}
    }
})


NOTICE_CONF = {
    'domain': {
        'email_conf': {
            'template': 'scanvmax_asset_will_expire',
            'params': ('user', 'time', 'num1')
        },
        'sms_conf': {'template': '4556998', 'params': ('num1', 'time')},
        'conditions': [('number', True)]
    },
    'httpMaxTask': {
        'email_conf': {
            'template': 'scanvmax_http_will_expire',
            'params': ('user', 'time', 'num', 'num1')
        },
        'sms_conf': {'template': '4556994', 'params': ('num', 'num1', 'time')},
        'conditions': [('number', True)]
    },
    'sitePortrait': {
        'email_conf': {
            'template': 'scanvmax_webportrait_will_expire',
            'params': ('user', 'time', 'num1')
        },
        'sms_conf': {'template': '4556988', 'params': ('num1', 'time')},
        'conditions': [('number', True)]
    },
    'maxPage': {
        'email_conf': {
            'template': 'scanvmax_url_will_expire',
            'params': ('user', 'time', 'num', 'num1')
        },
        'sms_conf': {'template': '4558918', 'params': ('num', 'num1', 'time')},
        'conditions': [('number', True)]
    },
    'manualTask': {
        'email_conf': {
            'template': 'scanvmax_single_will_expire',
            'params': ('user', 'time', 'num', 'num1')
        },
        'sms_conf': {'template': '4558924', 'params': ('num', 'num1', 'time')},
        'conditions': [('number', True)]
    },
    'smsAlert': {
        'email_conf': {
            'template': 'scanvmax_message_will_expire',
            'params': ('user', 'time', 'num', 'num1')
        },
        'sms_conf': {'template': '4558928', 'params': ('num', 'num1', 'time')},
        'conditions': [('number', True)]
    },
    'emailAlert': {
        'email_conf': {
            'template': 'scanvmax_email_will_expire',
            'params': ('user', 'time', 'num', 'num1')
        },
        'sms_conf': {'template': '4558936', 'params': ('num', 'num1', 'time')},
        'conditions': [('number', True)]
    },
}

EXPIRE_NOTICE_CONF = {
    'domain': {
        'email_conf': {
            'template': 'scanvmax_asset_has_expire',
            'params': ('user', 'time', 'num1', 'url')
        },
        'sms_conf': {'template': '4557000', 'params': ('num1', 'time')},
    },
    'httpMaxTask': {
        'email_conf': {
            'template': 'scanvmax_http_has_expire',
            'params': ('user', 'time', 'num', 'num1', 'num2', 'domain_list', 'url')
        },
        'sms_conf': {'template': '4556996', 'params': ('num', 'num1', 'time')},
    },
    'sitePortrait': {
        'email_conf': {
            'template': 'scanvmax_webportrait_has_expire',
            'params': ('user', 'time', 'num1', 'num2', 'domain_list', 'url')
        },
        'sms_conf': {'template': '4556990', 'params': ('num1', 'time')},
    },
    'maxPage': {
        'email_conf': {
            'template': 'scanvmax_url_has_expire',
            'params': ('user', 'time', 'num', 'num1')
        },
        'sms_conf': {'template': '4558922', 'params': ('num', 'num1', 'time')},
        'conditions': [('number', True)]
    },
    'manualTask': {
        'email_conf': {
            'template': 'scanvmax_single_has_expire',
            'params': ('user', 'time', 'num', 'num1')
        },
        'sms_conf': {'template': '4558926', 'params': ('num', 'num1', 'time')},
        'conditions': [('number', True)]
    },
    'smsAlert': {
        'email_conf': {
            'template': 'scanvmax_message_has_expire',
            'params': ('user', 'time', 'num', 'num1')
        },
        'sms_conf': {'template': '4558930', 'params': ('num', 'num1', 'time')},
        'conditions': [('number', True)]
    },
    'emailAlert': {
        'email_conf': {
            'template': 'scanvmax_email_has_expire',
            'params': ('user', 'time', 'num', 'num1')
        },
        'sms_conf': {'template': '4558938', 'params': ('num', 'num1', 'time')},
        'conditions': [('number', True)]
    },
}


class Balance(object):
    def __init__(self, collection):
        self.collection = collection
        self.data = self.collection.find()
        self.close_dict = {}
        self.notice_dict = {}
        self.update_list = []

        self.now_time = datetime.datetime.now()
        self.notice_day = [1, 3, 7, 10]
        self.is_reset_day = (self.now_time.day == 1)

    def travel(self):
        for _data in self.data:
            uid = _data.get('uid')

            balance = _data.get('balance', {})
            if _data.get('planExpireTime', self.now_time) < self.now_time:
                for k, v in balance.items():
                    v['charges'] = []
                    v['total'] = 0
                    if v['limitType'] == 'period':
                        v['used'] = 0
            else:
                for k, v in balance.items():
                    _temp_list = []
                    _used = v.get('used', 0)
                    _total = v.get('total', 0)
                    _charges = v.get('charges', [])

                    # if uid == bson.ObjectId("60ac584dc6142c001b7e11f9") and k == 'manualTask':
                    #     print(1)

                    if v.get('limitType') == 'limit':
                        v['total'], v['charges'], _notice_dict = self._limit(_total, _charges, k)
                    else:
                        v['used'], v['total'], v['charges'], _notice_dict = self._period(_used, _total, _charges, k)
                        v['used'] = 0 if v['used'] < 0 else v['used']

                    if _notice_dict:
                        self.notice_dict.setdefault(uid, {}).update(_notice_dict)

            # check used, total.
            for _name, setting in ENABLE_MODULES.items():
                module = balance.get(_name)
                if setting.get('has_enable'):
                    _enable_name = f"{_name}Enable"
                    module_enable = balance.get(_enable_name)
                    module_enable['total'] = module.get('total', 0)

                    _module_used = module_enable
                else:
                    module_enable = module

                _del = module_enable.get('used', 0) - module_enable.get('total', 0)
                if _del > 0:
                    self.close_dict.setdefault(uid, {})
                    self.close_dict[uid][_name] = _del
                    module_enable['used'] = module_enable['total']

            self.update_list.append(UpdateOne(
                {'_id': _data.get('_id')},
                {'$set': {'balance': balance}},
            ))

    def get_close_dict(self):
        """
        {
            "domain": {
                "2021-06-01 10:10:10": 3,
            }
        }
        Returns:

        """
        return self.close_dict

    def get_notice_dict(self):
        return self.notice_dict

    def save(self):
        if self.update_list:
            self.collection.bulk_write(self.update_list)

    def _limit(self, total, charges, module_name):
        new_charges = []
        notice_conf = NOTICE_CONF.get(module_name)
        notice = {}
        for _item in charges:
            if _item.get('chargeType') == 'additionPackage':
                _expire_time = _item.get('expireTime', self.now_time)
                # set notice
                self._set_notice(notice_conf, _expire_time, notice, _item)

                # is expire ?
                if _expire_time < self.now_time:
                    total -= _item.get('total', 0)
                else:
                    new_charges.append(_item)
            else:
                new_charges.append(_item)

        notice = {module_name: notice} if notice else {}

        return total, new_charges, notice

    def _period(self, used, total, charges, module_name):
        new_charges = []
        notice_conf = NOTICE_CONF.get(module_name)
        notice = {}
        for _item in charges:
            if _item.get('chargeType') == 'additionPackage':
                _expire_time = _item.get('expireTime', self.now_time)
                # set notice
                self._set_notice(notice_conf, _expire_time, notice, _item)

                if _expire_time < self.now_time:
                    used -= _item.get('used', 0)
                    total -= _item.get('total', 0)
                else:
                    if self.is_reset_day:
                        used -= _item.get('used', 0)
                        _item['used'] = 0
                    new_charges.append(_item)

            else:
                if self.is_reset_day:
                    used -= _item.get('used', 0)
                    _item['used'] = 0
                new_charges.append(_item)

        notice = {module_name: notice} if notice else {}

        return used, total, new_charges, notice

    def _get_notice_time(self, notice_list: list) -> dict:
        d0 = OrderedDict()
        for item in notice_list:
            d0[item] = self.now_time - datetime.timedelta(days=item)

        return d0

    def _set_notice(self, notice_conf, expire_time, notice: dict, charge):
        if notice_conf:
            for _cnd in notice_conf.get('conditions', []):
                _k0 = charge.get(_cnd[0])
                if isinstance(_cnd[1], bool):
                    bool_exp = bool(_k0) == bool(_cnd[1])
                else:
                    bool_exp = _k0 == _cnd[1]

                if not bool_exp:
                    return None

            days = (expire_time - self.now_time).days
            expire_date = expire_time.strftime(DATE_FMT)
            if days <= 0:
                notice = notice.setdefault('expire', {})
            elif days <= self.notice_day[-1] and days in self.notice_day:
                notice = notice.setdefault('notice', {})
            else:
                return

            info = notice.get(expire_date, {})
            if info:
                info['num1'] += int(charge.get('total', 0))
                info['num'] += int(charge.get('number', 0))
            else:
                info = {
                    'time': expire_date,
                    'num': 1,
                    'num1': charge.get('total', 0),
                    'num2': 0,
                    'domain_list': '暂无',
                    'url': DETAIL_URL
                }
            notice[expire_date] = info


class Job(object):
    """
    close_dict =
    {
        bson.ObjectId('604d7b060e2179001127f201'): {'domain': 5, 'httpMaxTask': 3, 'sitePortrait': 1},
        uid: module_list,
        ................
    ]
    """
    def __init__(self, collection, close_dict=None):
        self.collection = collection
        self.update_list = []
        self.data = dict()
        self.now_date = datetime.datetime.now()
        self.affect_urls = {}

        if close_dict:
            self.set_close_dict(close_dict)
        else:
            self.close_dict = {}

    def set_close_dict(self, close_dict):
        self.close_dict = close_dict
        for item in self.collection.find({'uid': {'$in': list(close_dict.keys())}}).sort('createTime', -1):
            uid = item.get('uid')
            if not uid:
                continue

            self.data.setdefault(uid, [])
            self.data[uid].append(item)

    def travel(self):
        for uid, module_dict in self.close_dict.items():
            check_params = self._get_check_params(module_dict)
            _temp_dict = {}
            for data in self.data.get(uid, []):
                for _module_name, _settings in check_params.items():
                    if (_close_num := _settings.get('close_num', 0)) > 0:
                        _url_list = []
                        if self._is_list(_settings.get('update_dict')) or self._is_list(_settings.get('conditions')):
                            _update_dict = {}
                            _stop_dict = {}
                            for i in range(_close_num):
                                if self._check_list_value(data, _settings.get('conditions'), i):
                                    for _update_k, _update_v in _settings.get('update_dict', {}).items():
                                        _update_dict[_update_k.format(index=i)] = _update_v
                                    if (_task_id := _settings.get('stop_params')['taskId']) == '*':
                                        _stop_dict['taskId'] = '*'
                                    else:
                                        _stop_dict['taskId'] = get_flat_value(data, _task_id.format(index=i))
                                    _stop_dict['jobId'] = data.get('_id')
                            if _update_dict:
                                self.update_list.append({
                                    'stop_dict': _stop_dict,
                                    'update_dict': UpdateOne({'_id': data.get('_id')}, {'$set': _update_dict})
                                })
                                _settings['close_num'] -= len(_update_dict.keys())
                                _url_list.append(
                                    (data.get('note', '未命名'), data.get('targetUrl', '')))
                        else:
                            if self._check_dict_value(data, _settings.get('conditions', [])):
                                _task_id = _settings.get('stop_params')['taskId']
                                _stop_dict = {
                                    'taskId': '*' if _task_id else get_flat_value(data, _task_id),
                                    'jobId': data.get('_id')
                                }
                                self.update_list.append({
                                    'stop_dict': _stop_dict,
                                    'update_dict': UpdateOne({'_id': data.get('_id')}, {'$set': _settings.get('update_dict')})
                                })
                                _settings['close_num'] -= 1
                                _url_list.append(
                                    (data.get('note', '未命名'), data.get('targetUrl', '')))
                            else:
                                continue
                        _temp_dict[_module_name] = _url_list
            self.affect_urls[uid] = _temp_dict

    def get_affect_url(self):
        return self.affect_urls

    def save(self):
        t_list = []
        monitor = Monitor()
        for update in self.update_list:
            t_list.append(StopTaskThread(self.collection, monitor, update['stop_dict'], update['update_dict']))
        for t in t_list:
            t.start()
        for t in t_list:
            t.join()

    def _travel_list(self, module_setting: list, name_dict: dict, upsert=False):
        pass

    def _get_check_params(self, module_dict: dict) -> (dict, dict):
        """
        check_params: {
            'httpMaxTask': {
                'update_dict': {'vulSettings': 'enableSitePortraitTriggerMonitor': False, 'enable': False},
                'conditions': [('enable', True}, ('vulSettings.enableSitePortraitTriggerMonitor': True)],
                'num': 3
            },
            module_name: {
                update_dict: dict(),
                conditions: list(),
                num: int
            }
            ..........................................
        }
        """
        check_params = dict()

        for _module, _num in module_dict.items():
            if _num == 0:
                continue

            check_params[_module] = {
                'update_dict': {},
                'conditions': [],
                'close_num': _num
            }
            _setting = ENABLE_MODULES.get(_module, {})
            if _setting:
                check_params[_module]['update_dict'] = _setting.get('update_dict', {})
                check_params[_module]['conditions'] = _setting.get('conditions', {})
                check_params[_module]['stop_params'] = _setting.get('stop_params', {})

        return check_params

    @staticmethod
    def _check_list_value(data, conditions, i):
        for cnd in conditions:
            _k_list = cnd[0].format(index=i).split('.')
            inner_data = data
            for _k in _k_list:
                if _k.isdigit():
                    if (j := int(_k)) >= len(inner_data):
                        return False
                    inner_data = inner_data[j]
                else:
                    inner_data = inner_data.get(_k)
                    if not _k:
                        return False
            if inner_data == cnd[1]:
                continue
            else:
                return False

        return True

    @staticmethod
    def _is_list(obj):
        s1 = json.dumps(obj)
        if re.match(r'.*\.{index}\..*', s1):
            return True

        return False

    @staticmethod
    def _check_dict_value(d0: dict, conditions: list) -> bool:
        for cnd in conditions:
            _inner_data = d0
            _check_list = cnd[0].split('.')
            for _check_k in _check_list:
                _inner_data = _inner_data.get(_check_k)
                if not _inner_data:
                    return False
            if not _inner_data == cnd[1]:
                return False

        return True


class AlertReceivers(object):
    def __init__(self, collection, collection_user):
        self.collection = collection
        self.collection_user = collection_user
        self.uid_map = {}
        self.notice_dict = {}
        self.affect_urls = {}
        self.close_dict = {}
        self.sendcloud = SendCloud()
        self.yunpian = YunPian()

    def set_notice_dict(self, notice_dict: dict):
        uid_list = list(notice_dict.keys())

        # get username map
        results = self.collection_user.find({'_id': {'$in': uid_list}})
        uname_map = {}
        for _result in results:
            uname_map[_result.get('_id')] = _result.get('username', '')

        results = self.collection.find({
            'enable': True,
            'isDefault': True,
            'uid': {'$in': uid_list},
            '$or': [{'phoneVerified': True}, {'emailVerified': True}]
        })
        for _result in results:
            _result['username'] = uname_map.get(_result.get('uid'), '')
            self.uid_map[_result.get('uid')] = _result

        self.notice_dict.update(notice_dict)

    def set_affect_urls(self, affect_urls):
        self.affect_urls = affect_urls

    def set_close_dict(self, close_dict):
        self.close_dict = close_dict

    def travel(self):
        for uid, _data in self.notice_dict.items():
            if user := self.uid_map.get(uid):
                for _module_name, _info in _data.items():
                    for _, __params in _info.get('notice', {}).items():
                        __email = user.get('email') if user.get('emailVerified', False) else None
                        __phone = user.get('phone') if user.get('phoneVerified', False) else None
                        self._send_notice_info(
                            _module_name, user.get('username'), __email, __phone, __params)

                    for _, __params in _info.get('expire', {}).items():
                        if __domain_list := self.affect_urls.get(uid, {}).get(_module_name, []):
                            __params['domain_list'] = self._trans_domain_list(__domain_list)
                        if __close_count := self.close_dict.get(uid, {}).get(_module_name, 0):
                            __params['num2'] = __close_count

                        __email = user.get('email') if user.get('emailVerified', False) else None
                        __phone = user.get('phone') if user.get('phoneVerified', False) else None
                        self._send_expire_info(
                            _module_name, user.get('username'), __email, __phone,  __params)

    @staticmethod
    def _get_uid_list(notice_list):
        uid_list = []
        for _notice in notice_list:
            uid_list.append(_notice.get('uid', ''))

        return uid_list

    def _send_notice_info(self, module_name, username, email, phone, params):
        conf = NOTICE_CONF.get(module_name, {})
        self._send_info(conf, username, email, phone, params)

    def _send_expire_info(self, module_name, username, email, phone, params):
        conf = EXPIRE_NOTICE_CONF.get(module_name, {})
        self._send_info(conf, username, email, phone, params)

    def _send_info(self, conf, username, email, phone, data):
        email_conf = conf.get('email_conf')
        sms_conf = conf.get('sms_conf')

        # send email
        if email:
            data['user'] = username
            params = {}
            for k in email_conf.get('params'):
                params[k] = data.get(k, '')

            args = {
                'to': [email],
                'sub': {f'%{k}%': [v] for k, v in params.items()}
            }
            self.sendcloud.sendmail(email_conf.get('template'), args)

        # send sms
        if phone:
            params = {}
            for k in sms_conf.get('params'):
                params[k] = data.get(k, '')

            args = '&'.join([f"#{k}#={v}" for k, v in params.items()])
            self.yunpian.tpl_send(phone, sms_conf.get('template'), args)

    @staticmethod
    def _trans_domain_list(domain_list):
        t_body = """
        <div style="width: 500px;margin: 0 auto; border: 1px solid #e7eaef;boder-radius: 2px;padding:10px 0">
            <div style="max-height: 100px; overflow-y:auto">
                <table>
	                <tbody>
	                {tr}
	                </tbody>
                </table>
            </div>
        </div>
        """
        tr = """
        <tr>
            <td style="padding:0 10px">{name}</td>
            <td style="padding:0 10px">{url}</td>
        </tr>
        """

        s0 = ""
        for domain in domain_list:
            s0 = f"{s0}{tr.format(name=domain[0], url=domain[1])}"

        return t_body.format(tr=s0).replace('\n', '')


class StopTaskThread(Thread):
    def __init__(self, collection, monitor: Monitor, stop_dict, update_dict):
        self._collection = collection
        self._monitor = monitor
        self._stop_dict = stop_dict
        self._update_dict = update_dict

        super(StopTaskThread, self).__init__()

    def run(self):
        request_args = {
            "jobId": str(self._stop_dict['jobId']),
            "taskIds": [str(self._stop_dict['taskId'])] if self._stop_dict['taskId'] != '*' else []
        }
        resp = self._monitor.post('/api/v2/jobs/tasks/remove', request_args)
        if resp and resp.status_code == 200:
            self._collection.bulk_write([self._update_dict])


def main():
    try:
        logger.info(f'Start {__file__}')
        with MongoClient(**DB_INFO) as conn:
            db = conn['cloudmonitor']
            collection_balances = db['balances']
            collection_jobs = db['jobs']
            collection_user = db['users']
            collection_alert = db['alertreceivers']

            balance = Balance(collection_balances)
            job = Job(collection_jobs)
            user = AlertReceivers(collection_alert, collection_user)

            balance.travel()
            close_dict = balance.get_close_dict()
            notice_dict = balance.get_notice_dict()

            job.set_close_dict(close_dict)
            job.travel()
            affect_urls = job.get_affect_url()

            job.save()
            balance.save()

            user.set_notice_dict(notice_dict)
            user.set_affect_urls(affect_urls)
            user.set_close_dict(close_dict)
            user.travel()

            logger.info(f'Complete {__file__}')
    except Exception as e:
        logger.exception(e)


if __name__ == '__main__':
    main()
